using System;
using System.CodeDom.Compiler;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.CSharp;
#if TEST
using Xunit;
#endif

namespace Fadd.Commands.Net
{
    /// <summary>
    /// Copies get/set properties from one object to another.
    /// </summary>
    internal class ObjectCopier : IDisposable
    {
        private const string BindingTemplate =
            @"{NameSpaces}

namespace Fadd.Commands.Net
{
    public class {TypeName}Binding : Binding
    {
        private static Type MyType = typeof({TypeName});

        public void Copy(object fromValue, object toValue)
        {
            {FullTypeName} from = ({FullTypeName})fromValue;
            {FullTypeName} to = ({FullTypeName})toValue;

{CopyProperties}
        }
    }
}
";

        private Dictionary<Type, Binding> _bindings = new Dictionary<Type, Binding>();

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="ObjectCopier"/> is reclaimed by garbage collection.
        /// </summary>
        ~ObjectCopier()
        {
            Dispose();
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            if (_bindings == null) return;
            _bindings.Clear();
            _bindings = null;
        }

        /// <summary>
        /// Copies all get/set properties from one object to another.
        /// </summary>
        /// <param name="from">From.</param>
        /// <param name="to">To.</param>
        public void Copy(object from, object to)
        {
            Check.Require(from, "from");
            Check.Require(to, "to");
            Type type = to.GetType();
            if (!type.IsAssignableFrom(from.GetType()))
                throw new ArgumentException(type.FullName + " must be assignable from " + from.GetType().FullName);

            lock (_bindings)
            {
                if (_bindings.ContainsKey(type))
                {
                    _bindings[type].Copy(from, to);
                    return;
                }
            }

            Binding binding = GenerateBinding(type);
            lock (_bindings)
            {
                if (!_bindings.ContainsKey(type))
                    _bindings.Add(type, binding);
            }

            binding.Copy(from, to);
        }

        private static Binding GenerateBinding(Type type)
        {
            string code = GenerateCode(type);
            return Compile(type, code);
        }

        private static string GenerateCode(Type type)
        {
            string properties = string.Empty;
            Type enumerableType = typeof (IEnumerable);

            PropertyInfo[] pis = type.GetProperties();
            foreach (PropertyInfo pi in pis)
            {
                if (pi.CanRead && pi.CanWrite)
                    properties += "\t\tto." + pi.Name + " = from." + pi.Name + ";\r\n";
                if (pi.CanWrite)
                    continue;

                // handle lists.
                Type itemType;
                if (pi.PropertyType.IsGenericType)
                {
                    Type[] types = pi.PropertyType.GetGenericArguments();
                    if (types.Length != 1)
                        throw new NotSupportedException("Tunnel does not support generic lists with multiple types.");
                    itemType = types[0];
                }
                else
                    itemType = typeof(object);

                MethodInfo addMethod = pi.PropertyType.GetMethod("Add", new Type[] {itemType});
                if (addMethod != null && enumerableType.IsAssignableFrom(pi.PropertyType))
                {
                    properties += "\t\tforeach (" + itemType + " value in from." + pi.Name + ")" + Environment.NewLine +
                                  "\t\t\tto." + pi.Name + ".Add(value);" + Environment.NewLine;
                }
            }

            string bindingCode = BindingTemplate;
            bindingCode = bindingCode.Replace("{NameSpaces}", "using " + type.Namespace + ";");
            bindingCode = bindingCode.Replace("{TypeName}", type.Name);
            bindingCode = bindingCode.Replace("{FullTypeName}", type.FullName.Replace("+", "."));
            bindingCode = bindingCode.Replace("{CopyProperties}", properties);
            return bindingCode;
        }

        #region Tests
#if TEST
        [Fact]
        private static void TestGenerateCode()
        {
            string code = GenerateCode(typeof (TestClass));
            const string expectedCode =
                @"using Fadd.Commands.Net;

namespace Fadd.Commands.Net
{
    public class TestClassBinding : Binding
    {
        public void Copy(object fromValue, object toValue)
        {
            Fadd.Commands.Net.ObjectCopier.TestClass from = (Fadd.Commands.Net.ObjectCopier.TestClass)fromValue;
            Fadd.Commands.Net.ObjectCopier.TestClass to = (Fadd.Commands.Net.ObjectCopier.TestClass)toValue;

		foreach (System.String value in from.Items)
			to.Items.Add(value);
		to.UserName = from.UserName;

        }
    }
}
";
            Assert.Equal(expectedCode, code);
        }

        // ReSharper disable UnusedMemberInPrivateClass
        /// <summary>
        /// </summary>
        public class TestClass
        {
            private readonly List<string> _items = new List<string>();
            private string _userName;

            /// <summary>
            /// Gets the items.
            /// </summary>
            /// <value>The items.</value>
            public List<string> Items
            {
                get { return _items; }
            }

            /// <summary>
            /// Gets or sets the name of the user.
            /// </summary>
            /// <value>The name of the user.</value>
            public string UserName

            {
                get { return _userName; }
                set { _userName = value; }
            }
        }
        // ReSharper restore UnusedMemberInPrivateClass
#endif
        #endregion
        private static Binding Compile(Type type, string code)
        {
            Check.Require(type, "type");
            Check.NotEmpty(code, "code");

            

            CSharpCodeProvider codeProvider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters();
            parameters.GenerateExecutable = false;
            parameters.GenerateInMemory = true;
            parameters.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location);
            parameters.ReferencedAssemblies.Add(type.Assembly.Location);

            CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, code);
            if (results.Errors.Count > 0)
            {
                string errs = "";

                foreach (CompilerError CompErr in results.Errors)
                    errs += "Line #" + CompErr.Line + ", " + CompErr.ErrorNumber + " '" + CompErr.ErrorText + "'" +
                            Environment.NewLine;
                InvalidOperationException err = new InvalidOperationException(errs);
                err.Data.Add("code", code);
                throw err;
            }

            string typeName = "Fadd.Commands.Net." + type.Name + "Binding";
            Assembly generatorAssembly = results.CompiledAssembly;
            return
                (Binding)
                generatorAssembly.CreateInstance(typeName, false, BindingFlags.CreateInstance,
                                                 null, null, null, null);
        }
    }
}
